跳到主要内容

protoreflect 反射 proto 文件

protoreflect 库是一个用来反射 proto 文件的库。它为 protobuf 和 gRPC 提供了反射 API。 protobuf 中反射的核心是 descriptor。 descriptor 本身就是 protobuf message,它描述 proto 源文件或其中的元素。 因此,descriptor 的集合可以描述 protobuf 类型的整个架构,包括 RPC 服务。

首先创建一个 protoparse.Parser,利用这个 Parser 去解析 proto 文件,得到 desc.FileDescriptor。之后根据 FileDescriptor 中的方法来提取出里面的 message 就好了。

// Parser parses proto source into descriptors.
type Parser struct {
// ...
}
// parse 文件并得到该文件的 FileDescriptor
func (p Parser) ParseFiles(filenames ...string) ([]*desc.FileDescriptor, error){
// ...
}

使用示例:

var fds []*desc.FileDescriptor
for _, filename := range fileNames {
fd, err := p.ParseFiles(filename)
if err != nil {
return nil, fmt.Errorf("不能解析文件: %v", err)
}

log.Tracef("解析文件:%s %d %s %+v", filename, len(fd), fd[0].GetPackage(),fd[0].GetServices())
fds = append(fds, fd...)
}

介绍下这个 FileDescriptor 结构体。它是对一个 proto 文件的描述,提供了很多 method,用于获取 proto 文件里的情况,函数的作用看名字就可以知道了。常用的就这四个 method。

GetMessageTypes() []*MessageDescriptor              // 获取文件内所有的 message 类型
GetEnumTypes() []*EnumDescriptor // 获取文件内所有的 enum 类型
FindMessage(msgName string) *MessageDescriptor // 根据名字来获取 message 类型
FindEnum(enumName string) *EnumDescriptor // 根据名字来获取 enum 类型

然后是 MessageDescriptor 结构体

它是对一个 proto message 的描述,也提供了很多 method。

GetName() string                                // message 的名字
GetFullyQualifiedName() string // message 的全限定名
AsDescriptorProto() *descriptor.DescriptorProto // AsDescriptorProto returns the underlying descriptor proto
GetFields() []*FieldDescriptor // 获取 message 内的所有字段
GetNestedMessageTypes() []*MessageDescriptor // 获取在 message 内内嵌的 message
GetNestedEnumTypes() []*EnumDescriptor
FindFieldByName(fieldName string) *FieldDescriptor
FindFieldByNumber(tagNumber int32) *FieldDescriptor
FindFieldByJSONName(jsonName string) *FieldDescriptor

最后是 FieldDescriptor 结构体,是对 message 内 field(字段)的描述。

GetName() string    
GetFullyQualifiedName() string
AsEnumDescriptorProto() *descriptor.EnumDescriptorProto
String() string
GetValues() []*EnumValueDescriptor // 获取该枚举所有的枚举值
FindValueByName(name string) *EnumValueDescriptor
FindValueByNumber(num int32) *EnumValueDescriptor

打印出 proto 文件内所有的 message

// test.proto
syntax = "proto3";
package test;

enum Sex{
Man = 0;
Woman = 1;
}

message Player{
int64 userId = 1;
string name = 2;
Sex sex = 3;
repeated int64 friends = 4;
}

message Monster{
// 怪物等级
int32 level = 1;
}
import (
"fmt"
"github.com/jhump/protoreflect/desc/protoparse"
)

func main() {
var parser protoparse.Parser
fileDescriptors, _ := parser.ParseFiles("./test.proto")
// 因为只有一个文件,所以肯定只有一个 fileDescriptor
fileDescriptor := fileDescriptors[0]
for _, msgDescriptor := range fileDescriptor.GetMessageTypes() {
fmt.Println(msgDescriptor.GetName())
for _, fieldDesc := range msgDescriptor.GetFields() {
fmt.Println("\t", fieldDesc.GetType().String(), fieldDesc.GetName())
}
fmt.Println()
}
for _, enumDescriptor := range fileDescriptor.GetEnumTypes() {
fmt.Println(enumDescriptor.GetName())
for _, valueDescriptor := range enumDescriptor.GetValues() {
fmt.Println("\t", valueDescriptor.GetName())
}
fmt.Println()
}
}

输出的字段名首字母都是小写的,例如userId,name,这是因为在proto文件中给他们定义时,首字母都是小写的。(不过通过proto文件最终生成的 proto.go 文件字段名的首字母都是大写的)

// Output
Player
TYPE_INT64 userId
TYPE_STRING name
TYPE_ENUM sex
TYPE_INT64 friends

Monster
TYPE_INT32 level

Sex
Man
Woman

为 proto message 生成其对应的 json 形式

package main

import (
"encoding/json"
"fmt"
"github.com/golang/protobuf/protoc-gen-go/descriptor"
"github.com/jhump/protoreflect/desc"
"github.com/jhump/protoreflect/desc/protoparse"
)
func main() {
var parser protoparse.Parser
fileDescriptors, _ := parser.ParseFiles("./test.proto")
// 因为只有一个文件,所以肯定只有一个 fileDescriptor
fileDescriptor := fileDescriptors[0]
m := make(map[string]interface{})
for _, msgDescriptor := range fileDescriptor.GetMessageTypes() {
m[msgDescriptor.GetName()] = convertMessageToMap(msgDescriptor)
}
bs, _ := json.MarshalIndent(m, "", "\t")
fmt.Println(string(bs))
}

func convertMessageToMap(message *desc.MessageDescriptor) map[string]interface{} {
m := make(map[string]interface{})
for _, fieldDescriptor := range message.GetFields() {
fieldName := fieldDescriptor.GetName()
if fieldDescriptor.IsRepeated() {
// 如果是一个数组的话,就返回 nil 吧
m[fieldName] = nil
continue
}
switch fieldDescriptor.GetType() {
case descriptor.FieldDescriptorProto_TYPE_MESSAGE:
m[fieldName] = convertMessageToMap(fieldDescriptor.GetMessageType())
continue
}
m[fieldName] = fieldDescriptor.GetDefaultValue()
}
return m
}
// Output
{
"Monster": {
"level": 0
},
"Player": {
"friends": null,
"name": "",
"sex": 0,
"userId": 0
},
"Union": {
"captain": {
"friends": null,
"name": "",
"sex": 0,
"userId": 0
}
}
}

References

Protocol Buffer and gRPC Reflection protoreflect库介绍-机智的小小帅